package scalikejdbc

import java.sql._
import util.control.Exception._
import scala.collection.compat._

/**
 * DB Session
 *
 * This class provides readOnly/autoCommit/localTx/withinTx blocks and session objects.
 *
 * {{{
 * import scalikejdbc._
 *
 * val userIdList = DB autoCommit { session: DBSession =>
 *   session.list("select * from user") { rs => rs.int("id") }
 * }
 * }}}
 */
trait DBSession extends LogSupport with LoanPattern with AutoCloseable {

  protected[scalikejdbc] def settings: SettingsProvider

  protected def unexpectedInvocation[A]: A = {
    throw new IllegalStateException("This method should not be called.")
  }

  /**
   * Connection
   */
  lazy val connection: Connection = conn

  private[scalikejdbc] val conn: Connection

  private[scalikejdbc] def connectionAttributes: DBConnectionAttributes

  /**
   * Returns current transaction if exists.
   */
  def tx: Option[Tx] = None

  @volatile
  private[this] var _fetchSize: Option[Int] = None

  @volatile
  private[this] var _tags: scala.collection.Seq[String] = Vector.empty

  @volatile
  private[this] var _queryTimeout: Option[Int] = None

  /**
   * is read-only session
   */
  val isReadOnly: Boolean

  private[this] def isAutoGeneratedKeyRetrievalWithColumnName(
    driverName: String
  ): Boolean = {
    settings
      .driverNamesToChooseColumnNameForAutoGeneratedKeyRetrieval(
        GlobalSettings.driverNamesToChooseColumnNameForAutoGeneratedKeyRetrieval
      )
      .contains(driverName)
  }

  /**
   * Create java.sql.Statement executor.
   *
   * @param conn connection
   * @param template SQL template
   * @param params parameters
   * @param returnGeneratedKeys is generated keys required
   * @param generatedKeyName generated key name
   * @return statement executor
   */
  private def createStatementExecutor(
    conn: Connection,
    template: String,
    params: scala.collection.Seq[Any],
    returnGeneratedKeys: Boolean = false,
    generatedKeyName: Option[String] = None
  ): StatementExecutor = {
    try {
      val statement = {
        if (returnGeneratedKeys) {
          (generatedKeyName, connectionAttributes.driverName) match {
            case (Some(key), Some(driverName))
              if isAutoGeneratedKeyRetrievalWithColumnName(driverName) =>
              conn.prepareStatement(template, Seq(key).toArray)
            case _ =>
              conn.prepareStatement(template, Statement.RETURN_GENERATED_KEYS)
          }
        } else {
          conn.prepareStatement(template)
        }
      }
      this.fetchSize.foreach { size => statement.setFetchSize(size) }
      this.queryTimeout.foreach { seconds =>
        statement.setQueryTimeout(seconds)
      }
      StatementExecutor(
        underlying = new DBConnectionAttributesWiredPreparedStatement(
          statement,
          connectionAttributes
        ),
        template = template,
        connectionAttributes = connectionAttributes,
        tags = tags,
        singleParams = params,
        settingsProvider = settings
      )

    } catch {
      case e: Exception =>
        val formattedTemplate =
          settings.sqlFormatter(GlobalSettings.sqlFormatter).formatter match {
            case Some(formatter) =>
              try {
                formatter.format(template)
              } catch {
                case e: Exception =>
                  log.debug("Failed to format SQL because " + e.getMessage, e)
                  template
              }
            case None =>
          }

        if (settings.loggingSQLErrors(GlobalSettings.loggingSQLErrors)) {
          log.error(
            "Failed preparing the statement (Reason: " + e.getMessage + "):\n\n  " + formattedTemplate + "\n"
          )
        } else {
          log.debug("Logging SQL errors is disabled.")
        }

        settings
          .queryFailureListener(GlobalSettings.queryFailureListener)
          .apply(template, params, e)
        settings
          .taggedQueryFailureListener(GlobalSettings.taggedQueryFailureListener)
          .apply(template, params, e, tags)

        throw e
    }
  }

  def toStatementExecutor(
    template: String,
    params: scala.collection.Seq[Any],
    returnGeneratedKeys: Boolean = false
  ): StatementExecutor = {
    createStatementExecutor(conn, template, params, returnGeneratedKeys)
  }

  /**
   * Create java.sql.Statement executor.
   * @param conn connection
   * @param template SQL template
   * @return statement executor
   */
  private def createBatchStatementExecutor(
    conn: Connection,
    template: String,
    returnGeneratedKeys: Boolean,
    generatedKeyName: Option[String]
  ): StatementExecutor = {
    val statement: PreparedStatement = {
      if (returnGeneratedKeys) {
        (generatedKeyName, connectionAttributes.driverName) match {
          case (Some(key), Some(driverName))
            if isAutoGeneratedKeyRetrievalWithColumnName(driverName) =>
            conn.prepareStatement(template, Seq(key).toArray)
          case _ =>
            conn.prepareStatement(template, Statement.RETURN_GENERATED_KEYS)
        }
      } else {
        conn.prepareStatement(template)
      }
    }
    this.fetchSize.foreach { size => statement.setFetchSize(size) }
    this.queryTimeout.foreach { seconds => statement.setQueryTimeout(seconds) }
    StatementExecutor(
      underlying = new DBConnectionAttributesWiredPreparedStatement(
        statement,
        connectionAttributes
      ),
      template = template,
      connectionAttributes = connectionAttributes,
      tags = tags,
      isBatch = true,
      settingsProvider = settings
    )
  }

  def toBatchStatementExecutor(template: String): StatementExecutor = {
    createBatchStatementExecutor(conn, template, false, None)
  }

  /**
   * Ensures the current session is not in read-only mode.
   * @param template
   */
  private def ensureNotReadOnlySession(template: String): Unit = {
    if (isReadOnly) {
      throw new java.sql.SQLException(
        ErrorMessage.CANNOT_EXECUTE_IN_READ_ONLY_SESSION + " (template:" + template + ")"
      )
    }
  }

  /**
   * Set fetchSize for this session.
   *
   * @param fetchSize fetch size
   * @return this
   */
  def fetchSize(fetchSize: Int): this.type = {
    this._fetchSize = Some(fetchSize)
    this
  }

  def fetchSize(fetchSize: Option[Int]): this.type = {
    this._fetchSize = fetchSize
    this
  }

  /**
   * Returns fetchSize for this session.
   *
   * @return fetch size
   */
  def fetchSize: Option[Int] = this._fetchSize

  /**
   * Set tags to this session.
   *
   * @param tags tags
   * @return this
   */
  def tags(tags: String*): this.type = {
    this._tags = tags
    this
  }

  /**
   * Returns tags for this session.
   *
   * @return tags
   */
  def tags: scala.collection.Seq[String] = this._tags

  /**
   * Set queryTimeout to this session.
   *
   * @param seconds query timeout seconds
   * @return this
   */
  def queryTimeout(seconds: Int): this.type = {
    this._queryTimeout = Some(seconds)
    this
  }

  def queryTimeout(seconds: Option[Int]): this.type = {
    this._queryTimeout = seconds
    this
  }

  /**
   * Returns queryTimeout for this session.
   *
   * @return query timeout seconds
   */
  def queryTimeout: Option[Int] = this._queryTimeout

  /**
   * Returns single result optionally.
   * If the result is not single, [[scalikejdbc.TooManyRowsException]] will be thrown.
   *
   * @param template SQL template
   * @param params parameters
   * @param extract extract function
   * @tparam A return type
   * @return result optionally
   */
  def single[A](template: String, params: Any*)(
    extract: WrappedResultSet => A
  ): Option[A] = {
    using(createStatementExecutor(conn, template, params)) { executor =>
      val proxy = new DBConnectionAttributesWiredResultSet(
        executor.executeQuery(),
        connectionAttributes
      )
      val resultSet = new ResultSetIterator(proxy)
      val rows = (resultSet map extract).toList
      rows match {
        case Nil        => None
        case one :: Nil => Option(one)
        case _          => throw new TooManyRowsException(1, rows.size)
      }
    }
  }

  /**
   * Returns the first row optionally.
   *
   * @param template SQL template
   * @param params parameters
   * @param extract extract function
   * @tparam A return type
   * @return result optionally
   */
  def first[A](template: String, params: Any*)(
    extract: WrappedResultSet => A
  ): Option[A] = {
    iterable(template, params*)(extract).headOption
  }

  /**
   * Returns query result as scala.List object.
   *
   * @param template SQL template
   * @param params parameters
   * @param extract extract function
   * @tparam A return type
   * @return result as list
   */
  def list[A](template: String, params: Any*)(
    extract: WrappedResultSet => A
  ): List[A] = {
    collection[A, List](template, params*)(extract)
  }

  /**
   * Returns query result as any Collection object.
   *
   * @param template SQL template
   * @param params parameters
   * @param extract extract function
   * @tparam A return type
   * @tparam C return collection type
   * @return result as C[A]
   */
  def collection[A, C[_]](template: String, params: Any*)(
    extract: WrappedResultSet => A
  )(implicit f: Factory[A, C[A]]): C[A] = {
    using(createStatementExecutor(conn, template, params)) { executor =>
      val proxy = new DBConnectionAttributesWiredResultSet(
        executor.executeQuery(),
        connectionAttributes
      )
      f.fromSpecific(new ResultSetIterator(proxy).map(extract))
    }
  }

  /**
   * Applies side-effect to each row iteratively.
   *
   * @param template SQL template
   * @param params parameters
   * @param f function
   * @return result as list
   */
  def foreach(template: String, params: Any*)(
    f: WrappedResultSet => Unit
  ): Unit = {
    using(createStatementExecutor(conn, template, params)) { executor =>
      val proxy = new DBConnectionAttributesWiredResultSet(
        executor.executeQuery(),
        connectionAttributes
      )
      new ResultSetIterator(proxy) foreach f
    }
  }

  /**
   * folding into one value.
   *
   * @param template SQL template
   * @param params parameters
   * @param z initial value
   * @param op function
   * @return folded value
   */
  def foldLeft[A](template: String, params: Any*)(
    z: A
  )(op: (A, WrappedResultSet) => A): A = {
    using(createStatementExecutor(conn, template, params)) { executor =>
      val proxy = new DBConnectionAttributesWiredResultSet(
        executor.executeQuery(),
        connectionAttributes
      )
      new ResultSetIterator(proxy).foldLeft(z)(op)
    }
  }

  /**
   * Returns query result as scala.collection.Iterable object.
   *
   * @param template SQL template
   * @param params parameters
   * @param extract extract function
   * @tparam A return type
   * @return result as iterable
   */
  def iterable[A](template: String, params: Any*)(
    extract: WrappedResultSet => A
  ): Iterable[A] = {
    collection[A, Iterable](template, params*)(extract)
  }

  /**
   * Executes java.sql.PreparedStatement#execute().
   *
   * @param template SQL template
   * @param params  parameters
   * @return flag
   */
  def execute(template: String, params: Any*): Boolean = {
    ensureNotReadOnlySession(template)
    using(createStatementExecutor(conn, template, params)) { _.execute() }
  }

  /**
   * Executes java.sql.PreparedStatement#execute().
   *
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return flag
   */
  def executeWithFilters(
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Boolean = {
    ensureNotReadOnlySession(template)
    using(createStatementExecutor(conn, template, params)) { executor =>
      before(executor.underlying)
      val result = executor.execute()
      after(executor.underlying)
      result
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeUpdate().
   *
   * @param template SQL template
   * @param params  parameters
   * @return result count
   */
  def executeUpdate(template: String, params: Any*): Int =
    update(template, params*)

  /**
   * Executes java.sql.PreparedStatement#executeUpdate().
   *
   * @param template SQL template
   * @param params parameters
   * @return result count
   */
  def update(template: String, params: Any*): Int = {
    ensureNotReadOnlySession(template)
    using(createStatementExecutor(conn, template, params)) { _.executeUpdate() }
  }

  /**
   * Executes java.sql.PreparedStatement#executeLargeUpdate().
   *
   * @param template SQL template
   * @param params parameters
   * @return result count
   */
  def executeLargeUpdate(template: String, params: Any*): Long = {
    ensureNotReadOnlySession(template)
    using(createStatementExecutor(conn, template, params)) {
      _.executeLargeUpdate()
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeUpdate().
   *
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def updateWithFilters(
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Int = updateWithFilters(false, before, after, template, params*)

  /**
   * Executes java.sql.PreparedStatement#executeUpdate().
   *
   * @param returnGeneratedKeys is generated keys required
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def updateWithFilters(
    returnGeneratedKeys: Boolean,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Int = updateWithFiltersInternal(
    returnGeneratedKeys = returnGeneratedKeys,
    before = before,
    after = after,
    template = template,
    execute = DBSession.executeUpdate,
    params
  )

  /**
   * Executes java.sql.PreparedStatement#executeLargeUpdate().
   *
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def largeUpdateWithFilters(
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Long = largeUpdateWithFilters(
    returnGeneratedKeys = false,
    before = before,
    after = after,
    template = template,
    params = params*
  )

  /**
   * Executes java.sql.PreparedStatement#executeLargeUpdate().
   *
   * @param returnGeneratedKeys is generated keys required
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def largeUpdateWithFilters(
    returnGeneratedKeys: Boolean,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Long = updateWithFiltersInternal(
    returnGeneratedKeys = returnGeneratedKeys,
    before = before,
    after = after,
    template = template,
    execute = DBSession.executeLargeUpdate,
    params
  )

  private[this] def updateWithFiltersInternal[A](
    returnGeneratedKeys: Boolean,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    execute: StatementExecutor => A,
    params: scala.collection.Seq[Any]
  ): A = {
    ensureNotReadOnlySession(template)
    using(
      createStatementExecutor(
        conn = conn,
        template = template,
        params = params,
        returnGeneratedKeys = returnGeneratedKeys
      )
    ) { executor =>
      before(executor.underlying)
      val count = execute(executor)
      after(executor.underlying)
      count
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeUpdate().
   *
   * @param returnGeneratedKeys is generated keys required
   * @param generatedKeyName generated key name
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def updateWithAutoGeneratedKeyNameAndFilters(
    returnGeneratedKeys: Boolean,
    generatedKeyName: String,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Int = updateWithAutoGeneratedKeyNameAndFiltersInternal(
    returnGeneratedKeys = returnGeneratedKeys,
    generatedKeyName = generatedKeyName,
    before = before,
    after = after,
    template = template,
    execute = DBSession.executeUpdate,
    params = params
  )

  /**
   * Executes java.sql.PreparedStatement#executeLargeUpdate().
   *
   * @param returnGeneratedKeys is generated keys required
   * @param generatedKeyName generated key name
   * @param before before filter
   * @param after after filter
   * @param template SQL template
   * @param params parameters
   * @return  result count
   */
  def largeUpdateWithAutoGeneratedKeyNameAndFilters(
    returnGeneratedKeys: Boolean,
    generatedKeyName: String,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    params: Any*
  ): Long = updateWithAutoGeneratedKeyNameAndFiltersInternal(
    returnGeneratedKeys = returnGeneratedKeys,
    generatedKeyName = generatedKeyName,
    before = before,
    after = after,
    template = template,
    execute = DBSession.executeLargeUpdate,
    params = params
  )

  private[this] def updateWithAutoGeneratedKeyNameAndFiltersInternal[A](
    returnGeneratedKeys: Boolean,
    generatedKeyName: String,
    before: PreparedStatement => Unit,
    after: PreparedStatement => Unit,
    template: String,
    execute: StatementExecutor => A,
    params: scala.collection.Seq[Any]
  ): A = {
    ensureNotReadOnlySession(template)
    using(
      createStatementExecutor(
        conn = conn,
        template = template,
        params = params,
        returnGeneratedKeys = returnGeneratedKeys,
        generatedKeyName = Option(generatedKeyName)
      )
    ) { executor =>
      before(executor.underlying)
      val count = execute(executor)
      after(executor.underlying)
      count
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeUpdate() and returns the generated key.
   *
   * @param template SQL template
   * @param params parameters
   * @return generated key as a long value
   */
  def updateAndReturnGeneratedKey(template: String, params: Any*): Long =
    updateAndReturnSpecifiedGeneratedKey(template, params*)(1)

  /**
   * Executes java.sql.PreparedStatement#executeUpdate() and returns the generated key.
   *
   * @param template SQL template
   * @param params parameters
   * @param key name
   * @return generated key as a long value
   */
  def updateAndReturnSpecifiedGeneratedKey(template: String, params: Any*)(
    key: Any
  ): Long = {
    var generatedKeyFound = false
    var generatedKey: Long = -1
    val before = (stmt: PreparedStatement) => {}
    val after = (stmt: PreparedStatement) => {
      val rs = stmt.getGeneratedKeys
      while (rs.next()) {
        generatedKeyFound = true
        generatedKey = key match {
          case name: String =>
            try {
              rs.getLong(name)
            } catch {
              case e: Exception =>
                log.warn(
                  "Failed to get generated key value via index " + name + ". Going to retrieve it via index 1."
                )
                rs.getLong(1)
            }
          case index: Int =>
            try {
              rs.getLong(index)
            } catch {
              case e: Exception =>
                log.warn(
                  "Failed to get generated key value via index " + index + ". Going to retrieve it via index 1."
                )
                rs.getLong(1)
            }
          case _ =>
            throw new IllegalArgumentException(
              ErrorMessage.FAILED_TO_RETRIEVE_GENERATED_KEY + "(key:" + key + ")"
            )
        }
      }
    }
    key match {
      case k: String =>
        updateWithAutoGeneratedKeyNameAndFilters(
          true,
          k,
          before,
          after,
          template,
          params*
        )
      case _ => updateWithFilters(true, before, after, template, params*)
    }
    if (!generatedKeyFound) {
      throw new IllegalStateException(
        ErrorMessage.FAILED_TO_RETRIEVE_GENERATED_KEY + " (template:" + template + ")"
      )
    }
    generatedKey
  }

  /**
   * Executes java.sql.PreparedStatement#executeBatch().
   * @param template SQL template
   * @param paramsList list of parameters
   * @return count list
   */
  def batch[C[_]](template: String, paramsList: scala.collection.Seq[Any]*)(
    implicit f: Factory[Int, C[Int]]
  ): C[Int] = {
    batchInternal[C, Int](
      template = template,
      paramsList = paramsList,
      execute = DBSession.executeBatch
    )
  }

  /**
   * Executes java.sql.PreparedStatement#executeLargeBatch().
   * @param template SQL template
   * @param paramsList list of parameters
   * @return count list
   */
  def largeBatch[C[_]](
    template: String,
    paramsList: scala.collection.Seq[Any]*
  )(implicit f: Factory[Long, C[Long]]): C[Long] =
    batchInternal[C, Long](
      template = template,
      paramsList = paramsList,
      execute = DBSession.executeLargeBatch
    )

  private[this] def batchInternal[C[_], A](
    template: String,
    paramsList: scala.collection.Seq[scala.collection.Seq[Any]],
    execute: StatementExecutor => scala.Array[A]
  )(implicit f: Factory[A, C[A]]): C[A] = {
    ensureNotReadOnlySession(template)
    paramsList match {
      case Nil => f.fromSpecific(Seq.empty[A])
      case _   =>
        using(
          createBatchStatementExecutor(
            conn = conn,
            template = template,
            returnGeneratedKeys = false,
            generatedKeyName = None
          )
        ) { executor =>
          paramsList.foreach { params =>
            executor.bindParams(params)
            executor.addBatch()
          }
          f.fromSpecific(execute(executor))
        }
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeBatch() and returns numeric generated keys.
   *
   * @param template SQL template
   * @param paramsList list of parameters
   * @return generated keys
   */
  def batchAndReturnGeneratedKey[C[_]](
    template: String,
    paramsList: scala.collection.Seq[Any]*
  )(implicit f: Factory[Long, C[Long]]): C[Long] = {
    ensureNotReadOnlySession(template)
    paramsList match {
      case Nil => f.fromSpecific(Seq.empty[Long])
      case _   =>
        using(
          createBatchStatementExecutor(
            conn = conn,
            template = template,
            returnGeneratedKeys = true,
            generatedKeyName = None
          )
        ) { executor =>
          paramsList.foreach { params =>
            executor.bindParams(params)
            executor.addBatch()
          }
          executor.executeBatch()
          f.fromSpecific(
            new ResultSetIterator(executor.generatedKeysResultSet)
              .map(_.long(1))
          )
        }
    }
  }

  /**
   * Executes java.sql.PreparedStatement#executeBatch() and returns numeric generated keys.
   *
   * @param template SQL template
   * @param key generated key name
   * @param paramsList list of parameters
   * @return generated keys
   */
  def batchAndReturnSpecifiedGeneratedKey[C[_]](
    template: String,
    key: String,
    paramsList: scala.collection.Seq[Any]*
  )(implicit f: Factory[Long, C[Long]]): C[Long] = {
    ensureNotReadOnlySession(template)
    paramsList match {
      case Nil => f.fromSpecific(Seq.empty[Long])
      case _   =>
        using(
          createBatchStatementExecutor(
            conn = conn,
            template = template,
            returnGeneratedKeys = true,
            generatedKeyName = Some(key)
          )
        ) { executor =>
          paramsList.foreach { params =>
            executor.bindParams(params)
            executor.addBatch()
          }
          executor.executeBatch()
          f.fromSpecific(
            new ResultSetIterator(executor.generatedKeysResultSet)
              .map(_.long(key))
          )
        }
    }
  }

  /**
   * Close the connection.
   */
  def close(): Unit = {
    ignoring(classOf[Throwable]) {
      conn.close()
    }
    if (settings.loggingConnections(GlobalSettings.loggingConnections)) {
      log.debug("A Connection is closed.")
    }
  }

}

object DBSession {

  private val executeUpdate: StatementExecutor => Int =
    _.executeUpdate()

  private val executeLargeUpdate: StatementExecutor => Long =
    _.executeLargeUpdate()

  private val executeBatch: StatementExecutor => scala.Array[Int] =
    _.executeBatch()

  private val executeLargeBatch: StatementExecutor => scala.Array[Long] =
    _.executeLargeBatch()

  def apply(
    conn: Connection,
    tx: Option[Tx] = None,
    isReadOnly: Boolean = false,
    connectionAttributes: DBConnectionAttributes = DBConnectionAttributes(),
    settings: SettingsProvider = SettingsProvider.default
  ): DBSession = {
    ActiveSession(conn, connectionAttributes, tx, isReadOnly, settings)
  }

}

/**
 * Active session implementation of [[scalikejdbc.DBSession]].
 *
 * This class provides readOnly/autoCommit/localTx/withinTx blocks and session objects.
 *
 * {{{
 * import scalikejdbc._
 *
 * val userIdList = DB autoCommit { session: DBSession =>
 *   session.list("select * from user") { rs => rs.int("id") }
 * }
 * }}}
 *
 * @param conn connection
 * @param tx transaction
 * @param isReadOnly is read only
 */
case class ActiveSession(
  private[scalikejdbc] val conn: Connection,
  private[scalikejdbc] val connectionAttributes: DBConnectionAttributes,
  override val tx: Option[Tx] = None,
  isReadOnly: Boolean = false,
  settings: SettingsProvider = SettingsProvider.default
) extends DBSession {

  tx match {
    case Some(tx) if tx.isActive() => // nothing to do
    case None                      =>
      if (
        !settings.jtaDataSourceCompatible(
          GlobalSettings.jtaDataSourceCompatible
        )
      ) {
        conn.setAutoCommit(true)
      }
    case _ =>
      throw new IllegalStateException(ErrorMessage.TRANSACTION_IS_NOT_ACTIVE)
  }
}

/**
 * Represents that there is no active session.
 */
case object NoSession extends DBSession {
  override private[scalikejdbc] val conn: Connection = null
  override val tx: Option[Tx] = None
  val isReadOnly: Boolean = false

  override def fetchSize(fetchSize: Int): this.type = unexpectedInvocation
  override def fetchSize(fetchSize: Option[Int]): this.type =
    unexpectedInvocation
  override def tags(tags: String*): this.type = unexpectedInvocation
  override def queryTimeout(seconds: Int): this.type = unexpectedInvocation
  override def queryTimeout(seconds: Option[Int]): this.type =
    unexpectedInvocation
  override private[scalikejdbc] lazy val connectionAttributes
    : DBConnectionAttributes = unexpectedInvocation
  override protected[scalikejdbc] def settings = SettingsProvider.default
}
